package xj.toolkit.util;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;

public class PackageUtils {
	private static final Logger logger = LoggerFactory.getLogger(PackageUtils.class);

	public static Map<URL, String[]> getAllCPResourceAsPathlike() throws IOException {
		Map<URL, String[]> vResult = new HashMap<URL, String[]>();
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (logger.isDebugEnabled()) {
			logger.debug("using classloader: " + cl);
		}
		Enumeration<URL> dirs = cl.getResources("");

		while (dirs.hasMoreElements()) {
			URL url = (URL) dirs.nextElement();
			String protocol = url.getProtocol();
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: url: " + url);
			}
			if ("file".equals(protocol)) {
				List<String> resources = new ArrayList<String>();
				getAllFileCPResourceAsPathlike("", URLDecoder.decode(url.getFile(), "UTF-8"), resources);
				vResult.put(url, resources.toArray(new String[0]));
			}
		}
		Enumeration<URL> jars = cl.getResources("META-INF");
		while (jars.hasMoreElements()) {
			URL url = (URL) jars.nextElement();
			String protocol = url.getProtocol();
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: url: " + url);
			}
			if ("jar".equals(protocol)) {
				List<String> resources = new ArrayList();
				JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();

				Enumeration<JarEntry> entries = jar.entries();
				while (entries.hasMoreElements()) {
					JarEntry entry = (JarEntry) entries.nextElement();
					String name = entry.getName();
					if (name.charAt(0) == '/') {
						name = name.substring(1);
					}
					if (!entry.isDirectory()) {
						resources.add(name);
					}
				}
				vResult.put(url, resources.toArray(new String[0]));
			}
		}
		return vResult;
	}

	private static void getAllFileCPResourceAsPathlike(String basePath, String filePath, List<String> resources) {
		File dir = new File(filePath);
		if ((!dir.exists()) || (!dir.isDirectory())) {
			return;
		}
		File[] dirfiles = dir.listFiles();
		for (File file : dirfiles) {
			if (file.isDirectory()) {
				getAllFileCPResourceAsPathlike(basePath + file.getName() + "/", file.getAbsolutePath(), resources);
			} else {
				resources.add(basePath + file.getName());
			}
		}
	}

	public static String[] getClasspathResourceAsPathlike(String path) throws IOException {
		String packageOnly = path;
		boolean recursive = false;
		if (path.endsWith(".*")) {
			packageOnly = path.substring(0, path.lastIndexOf(".*"));
			recursive = true;
		}
		if (packageOnly.endsWith("/")) {
			packageOnly = packageOnly.substring(0, path.length() - 1);
		}
		List<String> vResult = new ArrayList();
		String packageDirName = packageOnly.replace('.', '/');
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (logger.isDebugEnabled()) {
			logger.debug("using classloader: " + cl);
		}
		Enumeration<URL> dirs = cl.getResources(packageDirName);
		if (logger.isDebugEnabled()) {
			logger.debug("PackageUtils: getResources: " + dirs + ", hasMoreElements:" + dirs.hasMoreElements());
		}
		while (dirs.hasMoreElements()) {
			URL url = (URL) dirs.nextElement();
			String protocol = url.getProtocol();
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: url: " + url);
			}
			if ("file".equals(protocol)) {
				getDirCPResourceAsPathlike(packageOnly, URLDecoder.decode(url.getFile(), "UTF-8"), recursive, vResult);
			} else if ("jar".equals(protocol)) {
				JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();

				Enumeration<JarEntry> entries = jar.entries();
				while (entries.hasMoreElements()) {
					JarEntry entry = (JarEntry) entries.nextElement();
					String name = entry.getName();
					if (name.charAt(0) == '/') {
						name = name.substring(1);
					}
					if ((name.startsWith(packageDirName)) && (!entry.isDirectory())) {
						vResult.add(name);
					}
				}
			} else {
				String urlpath = url.getPath();
				if (urlpath.startsWith(packageDirName)) {
					vResult.add(urlpath);
				}
			}
		}
		return (String[]) vResult.toArray(new String[vResult.size()]);
	}

	private static void getDirCPResourceAsPathlike(String packageName, String packagePath, boolean recursive,
			List<String> classes) {
		File dir = new File(packagePath);
		if ((!dir.exists()) || (!dir.isDirectory())) {
			return;
		}
		File[] dirfiles = dir.listFiles();
		for (File file : dirfiles) {
			if (file.isDirectory()) {
				getDirCPResourceAsPathlike(packageName + "/" + file.getName(), file.getAbsolutePath(), recursive,
						classes);
			} else {
				classes.add(packageName + "/" + file.getName());
			}
		}
	}

	public static String[] getResourceInPackage(String packageName) throws IOException {
		String packageOnly = packageName;
		boolean recursive = false;
		
		if (packageName.endsWith(".*")) {
			packageOnly = packageName.substring(0, packageName.lastIndexOf(".*"));
			recursive = true;
		}
		
		if (packageOnly.endsWith("/")) {
			packageOnly = packageOnly.substring(0, packageName.length() - 1);
		}
		
		List<String> vResult = new ArrayList();
		String packageDirName = packageOnly.replace('.', '/');
		ClassLoader cl = Thread.currentThread().getContextClassLoader();
		if (logger.isDebugEnabled()) {
			logger.debug("using classloader: " + cl + ", current thread[" + Thread.currentThread() + "].");
		}
		Enumeration<URL> dirs = cl.getResources(packageDirName);
		if (logger.isDebugEnabled()) {
			logger.debug("PackageUtils: getResources: " + dirs + ", hasMoreElements:" + dirs.hasMoreElements());
		}
		while (dirs.hasMoreElements()) {
			URL url = (URL) dirs.nextElement();
			String protocol = url.getProtocol();
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: url: " + url);
			}
			if ("file".equals(protocol)) {
				getResourceInDirPackage(packageOnly, URLDecoder.decode(url.getFile(), "UTF-8"), recursive, vResult);
			} else if ("jar".equals(protocol)) {
				JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();

				Enumeration<JarEntry> entries = jar.entries();
				while (entries.hasMoreElements()) {
					JarEntry entry = (JarEntry) entries.nextElement();
					String name = entry.getName();
					if (name.charAt(0) == '/') {
						name = name.substring(1);
					}
					if (name.startsWith(packageDirName)) {
						int idx = name.lastIndexOf('/');
						if (idx != -1) {
							packageName = name.substring(0, idx).replace('/', '.');
						}
						if (logger.isDebugEnabled()) {
							logger.debug("PackageUtils: Package name is " + packageName);
						}
						if ((idx != -1) || (recursive)) {
							if (!entry.isDirectory()) {
								String resName = name.substring(packageName.length() + 1);

								vResult.add(packageName + "." + resName);
							}
						}
					}
				}
			} else {
				String path = url.getPath();
				if (path.startsWith(packageDirName)) {
					vResult.add(path.replace('/', '.'));
				}
			}
		}
		String[] result = (String[]) vResult.toArray(new String[vResult.size()]);
		return result;
	}

	private static void getResourceInDirPackage(String packageName, String packagePath, boolean recursive,
			List<String> classes) {
		File dir = new File(packagePath);
		if ((!dir.exists()) || (!dir.isDirectory())) {
			return;
		}
		File[] dirfiles = dir.listFiles();
		for (File file : dirfiles) {
			if (file.isDirectory()) {
				getResourceInDirPackage(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);
			} else {
				classes.add(packageName + "." + file.getName());
			}
		}
	}

	private static void findClassesInDirPackage(String packageName, List<String> included, List<String> excluded,
			String packagePath, boolean recursive, List<String> classes) {
		File dir = new File(packagePath);
		if ((!dir.exists()) || (!dir.isDirectory())) {
			return;
		}
		File[] dirfiles = dir.listFiles(new FileFilter() {
			public boolean accept(File file) {
				return (file.isDirectory()) || (file.getName().endsWith(".class"));
			}
		});
		if (logger.isDebugEnabled()) {
			logger.debug("PackageUtils: Looking for test classes in the directory: " + dir);
		}
		for (File file : dirfiles) {
			if (file.isDirectory()) {
				findClassesInDirPackage(packageName + "." + file.getName(), included, excluded, file.getAbsolutePath(),
						recursive, classes);
			} else {
				String className = file.getName().substring(0, file.getName().length() - 6);
				if (logger.isDebugEnabled()) {
					logger.debug("PackageUtils: Found class " + className + ", seeing it if it's included or excluded");
				}
				includeOrExcludeClass(packageName, className, included, excluded, classes);
			}
		}
	}

	public static String[] findClassesInPackage(String packageName, List<String> included, List<String> excluded,
			ClassLoader cl) throws IOException {
		String packageOnly = packageName;
		boolean recursive = false;
		if (packageName.endsWith(".*")) {
			packageOnly = packageName.substring(0, packageName.lastIndexOf(".*"));

			recursive = true;
		}
		List<String> vResult = new ArrayList();
		String packageDirName = packageOnly.replace('.', '/');
		if (logger.isDebugEnabled()) {
			logger.debug("using classloader: " + cl);
		}
		Enumeration<URL> dirs = cl.getResources(packageDirName);
		if (logger.isDebugEnabled()) {
			logger.debug("PackageUtils: getResources: " + dirs + ", hasMoreElements:" + dirs.hasMoreElements());
		}
		while (dirs.hasMoreElements()) {
			URL url = (URL) dirs.nextElement();
			String protocol = url.getProtocol();
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: url: " + url);
			}
			if ("file".equals(protocol)) {
				findClassesInDirPackage(packageOnly, included, excluded, URLDecoder.decode(url.getFile(), "UTF-8"),
						recursive, vResult);
			} else if ("jar".equals(protocol)) {
				JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();

				Enumeration<JarEntry> entries = jar.entries();
				while (entries.hasMoreElements()) {
					JarEntry entry = (JarEntry) entries.nextElement();
					String name = entry.getName();
					if (name.charAt(0) == '/') {
						name = name.substring(1);
					}
					if (name.startsWith(packageDirName)) {
						int idx = name.lastIndexOf('/');
						if (idx != -1) {
							packageName = name.substring(0, idx).replace('/', '.');
						}
						if (logger.isDebugEnabled()) {
							logger.debug("PackageUtils: Package name is " + packageName);
						}
						if ((idx != -1) || (recursive)) {
							if ((name.endsWith(".class")) && (!entry.isDirectory())) {
								String className = name.substring(packageName.length() + 1, name.length() - 6);
								if (logger.isDebugEnabled()) {
									logger.debug("PackageUtils: Found class " + className
											+ ", seeing it if it's included or excluded");
								}
								includeOrExcludeClass(packageName, className, included, excluded, vResult);
							}
						}
					}
				}
			} else {
				String path = url.getPath();
				if ((path.startsWith(packageDirName)) && (path.endsWith(".class"))) {
					String pkg = "";
					int idx = path.lastIndexOf('/');
					if (idx != -1) {
						pkg = path.substring(0, idx).replace('/', '.');
					}
					String className = path.substring(pkg.length() + 1, path.length() - 6);
					includeOrExcludeClass(pkg, className, included, excluded, vResult);
				}
			}
		}
		String[] result = (String[]) vResult.toArray(new String[vResult.size()]);
		return result;

	}

	public static String[] findClassesInPackage(String packageName, List<String> included, List<String> excluded)
			throws IOException {
		return findClassesInPackage(packageName, included, excluded, Thread.currentThread().getContextClassLoader());
	}

	private static void includeOrExcludeClass(String packageName, String className, List<String> included,
			List<String> excluded, List<String> classes) {
		if (isIncluded(className, included, excluded)) {
			if (logger.isDebugEnabled()) {
				logger.debug("PackageUtils: ... Including class " + className);
			}
			classes.add(packageName + '.' + className);
		} else if (logger.isDebugEnabled()) {
			logger.debug("PackageUtils: ... Excluding class " + className);
		}
	}

	private static boolean isIncluded(String name, List<String> included, List<String> excluded) {
		boolean result = false;
		if (null == included) {
			included = Lists.newArrayList();
		}
		if (null == excluded) {
			excluded = Lists.newArrayList();
		}
		if ((included.size() == 0) && (excluded.size() == 0)) {
			result = true;
		} else {
			boolean isIncluded = find(name, included);
			boolean isExcluded = find(name, excluded);
			if ((isIncluded) && (!isExcluded)) {
				result = true;
			} else if (isExcluded) {
				result = false;
			} else {
				result = included.size() == 0;
			}
		}
		return result;
	}

	private static boolean find(String name, List<String> list) {
		for (String regexpStr : list) {
			if (Pattern.matches(regexpStr, name)) {
				return true;
			}
		}
		return false;
	}

}
